feat(simulator): backend-aware createSimulator (dry + live)#121
Conversation
Make the per-module simulator runnable against a live Midnight node as well as the in-memory path, selected by MIDNIGHT_BACKEND=dry|live at construction. One factory (createSimulator) now returns an async, backend-aware class; circuits return promises so a single spec file runs on both backends. * core: a Backend<P,L> seam with DryBackend (a thin async facade over the existing synchronous engine, now exposed internally as createDrySimulator) and a dynamically-imported LiveBackend adapter over an injected LiveContext; an AsyncCircuits<> mapped type for the proxies. * live: a createLiveContext assembler (per-alias handle cache + bounded indexer-lag reads) and a registerLiveBackend/isLiveBackend registry, so a test:live setup wires the harness once and specs stay backend-agnostic. * signers: alias resolver with deterministic dry keys and a 4-signer live cap. * dependency wall: every @midnight-ntwrk/midnight-js import is confined to src/live/ and reached only via dynamic import, so a dry import resolves zero midnight-js (guarded by a test); they are declared optional peers. BREAKING CHANGE: createSimulator is now async. Construct with `await Sim.create(args, options)` and await circuits and state getters. The previous synchronous surface is replaced; the in-memory engine remains available internally as createDrySimulator.
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughIntroduces a ChangesSimulator Live-Mode Backend Abstraction
Sequence Diagram(s)sequenceDiagram
participant TestSuite
participant ContractSimulator
participant createSimulator
participant DryBackend
participant LiveBackend
participant LiveContext
TestSuite->>ContractSimulator: static create(args, options)
ContractSimulator->>createSimulator: prepareBackend(config, args, options)
alt MIDNIGHT_BACKEND=dry
createSimulator->>DryBackend: new DryBackend(syncSim, signers)
DryBackend-->>createSimulator: backend (kind='dry')
else MIDNIGHT_BACKEND=live
createSimulator->>createSimulator: dynamic import LiveBackend
createSimulator->>LiveContext: options.live or getRegisteredLiveBackend()(request)
LiveContext-->>createSimulator: LiveContext<P>
createSimulator->>LiveBackend: new LiveBackend({ ctx, pureSim, signers, ledgerExtractor })
LiveBackend-->>createSimulator: backend (kind='live')
end
createSimulator-->>ContractSimulator: BackendDeps
ContractSimulator-->>TestSuite: simulator instance
TestSuite->>ContractSimulator: circuits.impure.someMethod(args)
ContractSimulator->>DryBackend: call('impure', 'someMethod', args)
DryBackend-->>ContractSimulator: Promise<result>
ContractSimulator-->>TestSuite: result
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Add the midnight-js optional peer/dev deps to yarn.lock so the immutable CI install matches package.json. Remove the design/invariants/code pipeline artifacts under packages/simulator/docs from the PR (kept locally, out of the published package).
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
packages/simulator/docs/design/live-backend.md (1)
68-85: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueSpecify language for fenced code block.
The fenced code block starting at line 68 should specify a language identifier for proper syntax highlighting and tooling support.
Add a language identifier (e.g.,
textorplaintext):-``` +```text packages/simulator/src/🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/simulator/docs/design/live-backend.md` around lines 68 - 85, The fenced code block displaying the directory structure is missing a language identifier for proper syntax highlighting. Change the opening fence marker from ``` to ```text at the beginning of the block that shows the packages/simulator/src/ directory layout to specify the content type and enable proper rendering.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/simulator/docs/design/live-backend.md`:
- Line 25: The design document references a dual-factory approach with both a
new `createBackendSimulator` and the existing synchronous factory, but the
actual implementation unified these into a single factory. Update the design
document in the live-backend.md file to describe the unified API where
`createSimulator` itself is now the async, backend-aware factory, and remove
references to a separate `createBackendSimulator` factory. Alternatively, add a
note explaining that the dual-factory design was rejected in favor of the single
unified factory approach documented in the code documentation.
In `@packages/simulator/src/live/LiveBackend.ts`:
- Around line 78-105: The call method in LiveBackend currently calls
consumeSingle() after both pure and impure circuit executions, which causes
inconsistent lifecycle management. Remove the consumeSingle() call from the pure
path where it runs after the fn() result is returned. For the impure path, wrap
the txFn execution and result handling in a try-finally block, moving the
consumeSingle() call into the finally block so it executes regardless of whether
the async txFn call succeeds or rejects. This ensures consumption happens
consistently for all impure calls and doesn't occur for pure calls, preventing
leaked single aliases across failed impure calls.
In `@packages/simulator/src/live/registry.ts`:
- Around line 46-48: The registerLiveBackend function currently silently
overwrites an existing registeredFactory without any warning or check, which can
cause non-deterministic behavior in shared test processes. Add a guard check
before assigning the factory parameter to registeredFactory that throws an error
if a factory is already registered, ensuring that accidental or duplicate
registrations are caught and prevent silent context switching. This will make
the function fail loudly rather than silently replacing the backend factory.
In `@packages/simulator/src/signers/Signers.ts`:
- Around line 95-100: The validation block checking if `this.liveAliases.size >
MAX_LIVE_SIGNERS` is executed regardless of the current mode, which causes
shared option objects to fail in dry mode even though liveAliases is documented
as live-only. Gate this validation by wrapping the entire error-throwing block
in a conditional that checks if the mode is 'live' before executing it, so the
signer-cap validation only enforces the limit when actually running in live
mode.
In `@packages/simulator/test/unit/dependency-wall.test.ts`:
- Around line 40-41: The startsWith(LIVE_DIR) check on line 40 uses string
prefix matching which creates false positives for similarly-named directories,
such as treating src/live2/ as being inside src/live/. Replace the startsWith
check with a more robust path boundary check that ensures LIVE_DIR is followed
by a path separator (forward slash) to prevent forbidden imports from bypassing
this guard and ensure only files actually within the LIVE_DIR directory are
filtered out.
---
Nitpick comments:
In `@packages/simulator/docs/design/live-backend.md`:
- Around line 68-85: The fenced code block displaying the directory structure is
missing a language identifier for proper syntax highlighting. Change the opening
fence marker from ``` to ```text at the beginning of the block that shows the
packages/simulator/src/ directory layout to specify the content type and enable
proper rendering.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: e1e1bfb2-ca4b-48d8-847d-e142b296c424
📒 Files selected for processing (27)
packages/simulator/docs/code/live-backend-code.mdpackages/simulator/docs/design/live-backend-invariants.mdpackages/simulator/docs/design/live-backend.mdpackages/simulator/package.jsonpackages/simulator/src/backend/Backend.tspackages/simulator/src/backend/DryBackend.tspackages/simulator/src/factory/SimulatorConfig.tspackages/simulator/src/factory/createDrySimulator.tspackages/simulator/src/factory/createSimulator.tspackages/simulator/src/index.tspackages/simulator/src/live/LiveBackend.tspackages/simulator/src/live/LiveContext.tspackages/simulator/src/live/createLiveContext.tspackages/simulator/src/live/registry.tspackages/simulator/src/signers/Signers.tspackages/simulator/src/types/Circuit.tspackages/simulator/src/types/Options.tspackages/simulator/src/types/index.tspackages/simulator/test/integration/SampleZOwnable.test.tspackages/simulator/test/integration/SampleZOwnableSimulator.tspackages/simulator/test/integration/Simple.test.tspackages/simulator/test/integration/SimpleSimulator.tspackages/simulator/test/integration/Witness.test.tspackages/simulator/test/integration/WitnessSimulator.tspackages/simulator/test/unit/LiveBackendAdapter.test.tspackages/simulator/test/unit/Signers.test.tspackages/simulator/test/unit/dependency-wall.test.ts
Remove the INV-N citation tags that referenced the live-backend invariants design doc from all simulator source and test comments, reflowing each sentence so it reads naturally on its own. Non-INV design references (D1, D2, OQ2/4/6) are left intact. Also normalize a stray NUL byte in createLiveContext.ts to its unicode escape sequence, so the file is valid UTF-8 text that git diffs normally instead of being treated as binary. The runtime string value is unchanged.
* signers: gate the MAX_LIVE_SIGNERS cap check on live mode, so a shared options object carrying an oversized `liveAliases` no longer throws at construction in dry mode (the field is documented as live-only). * registry: make `registerLiveBackend` throw when a different factory is already registered instead of silently replacing it; a silent swap in a shared test process switches harness context and yields non-deterministic live runs. Re-registering the same factory is a no-op; call `clearLiveBackend` to replace. * dependency-wall test: match the live directory with a trailing path separator so the prefix check can't treat a sibling like `src/live2/` as inside `src/live/`.
andrew-fleming
left a comment
There was a problem hiding this comment.
Discussed the PR with @0xisk offline. LGTM for an alpha release, will provide a full review thereafter
The async backend-aware simulator (OpenZeppelin/compact-tools#121) is now published as @openzeppelin/compact-simulator@0.2.0. Replace the placeholder `portal:` dependency on a sibling compact-tools checkout with the published package, so the default `test` CI can install the dependency and run the dry suite (the `portal:` link could not resolve in CI). Validated dry on the utils, access, token, and security modules (298 tests green) against the published API; the imported surface (createSimulator, registerLiveBackend, LiveBackendRequest, LiveContext, SimulatorOptions) is unchanged from the portal build.
Types of changes
Refs #75
Adds a live backend to
@openzeppelin/compact-simulatorso the sameper-module simulator runs against either the in-memory path or a live local
Midnight node, selected by
MIDNIGHT_BACKEND=dry|liveat construction.One factory —
createSimulator— now returns an async, backend-aware class;circuits return promises, so a single spec file runs on both backends with
uniform
await.MIDNIGHT_BACKEND=dry(default) is the existing in-memorybehaviour;
MIDNIGHT_BACKEND=liveattaches to a node via a caller-providedharness.
What's in it
Backend<P,L>withDryBackend(a thin async facade overthe unchanged synchronous engine, now
createDrySimulator) and adynamically-imported
LiveBackendadapter over an injectedLiveContext.AsyncCircuits<>mapped type for the proxies.createLiveContextassembler (per-alias handle cache +bounded indexer-lag reads) and a
registerLiveBackend/isLiveBackendregistry, so a
test:livesetup wires the harness once and specs staybackend-agnostic (
await Sim.create()identical on both;it.skipIf(isLiveBackend())guards the documented dry↔live asymmetries).
@midnight-ntwrk/midnight-jsimport is confined tosrc/live/and reached only via dynamic import, so a dry import resolves zeromidnight-js (guarded by a test); declared as optional peer deps.
Breaking change
createSimulatoris now async: construct withawait Sim.create(args, options)and await circuits/state getters. The previous synchronous surface is replaced;
the in-memory engine remains available internally as
createDrySimulator.Consumers migrate per-module (mechanical async migration).
Validation
tscclean; build clean.dist/index.jsresolves 0 realmidnight-js imports.
compact-contracts): swapped in, the existing1154 passing unit tests are unaffected; the migrated
securitymodule runsgreen on
dryand green onliveagainst a local node (make env-up—real deploy → prove → submit tx → indexer read → assert).
PR Checklist
Further comments
The single-async-factory shape (vs a separate
createBackendSimulator) was chosenso consumers keep one simulator file and one test file per module — no duplicate
*Backendtwins — at the cost of a one-time async migration. Design / invariants /code artifacts are under
packages/simulator/docs/.Summary by CodeRabbit
New Features
Promise-based circuit operations and factory-pattern construction.MIDNIGHT_BACKEND=dry|liveenvironment variable.Tests